/**
  @file: Log.java

  PURPOSE: 
  Implements a system and diagnostic log.

  @author Bill Kraemer

  REVISION HISTORY ABSTRACT

  PROGRAMMER:  Bill Kraemer         ORG: MSE

  Copyright 2011 Mission Solutions LLC. All rights reserved.

  END ABSTRACT
 */
package com.mse.utilities;

import java.io.*;
import java.util.EnumMap;
import java.util.Map;
import java.util.logging.*;

/**
  DESCRIPTION:
  This class implements a system and diagnostic log.
  <p>
  The Log initializes itself via the Settings class. The following Settings
  are supported for Log configuration:
  <ul>
  <li>Log_Level = level used to control logging output (OFF,ERROR,WARNING,INFO,DEBUG)</li>
  <li>Log_ConsoleEnabled = flag to enable/disable logging via console handler</li>
  <li>Log_FileEnabled = flag to enable/disable logging via file handler</li>
  <li>Log_FileLimit = maximum size of each log file</li>
  <li>Log_FileCount = number of rotated log files to cycle through</li>
  <li>Log_FileAppend = flag to enable/disable file handler append mode</li>
  <li>Log_FilePattern = pattern for generating log file names</li>
  <ul>
  <li>%h = user.home system property
  <li>%g = generation number to distinguish rotated logs</li>
  <li>%t = system temporary directory</li>
  <li>%u = unique number to resolve conflicts
  </ul>
  </ul>
 */
public class Log {
   
   ///< log levels
   private enum LogLevel {
      OFF,          ///< logging disabled
      ERROR,        // (highest) any serious error that must be brought to admin attention
      WARNING,      // an important indication of problem or potential failure
      INFO,         // an informational diagnostic message
      DEBUG,        // (lowest) all other diagnostic messages
   }
   
   ///< singleton instance
   private static Log instance;

   ///< wrapped java logger instance
   private Logger logger; 
   
   ///< flag to enable/disable logging via file handler 
   private boolean fileEnabled;
   
   ///< pattern for generating log file names
   private String filePattern;
   
   ///< max size of each log file in bytes
   private int fileLimit;
   
   ///< number of rotated log files to cycle through
   private int fileCount;
   
   ///< flag to enable/disable file handler append mode
   private boolean fileAppend;
  
   ///< flag to enable/disable logging via console handler
   private boolean consoleEnabled;
   
   ///< log output level
   private LogLevel level;
   
   ///< map our levels to Logger levels
   private Map<LogLevel,Level> levelMap;
   
   /**
     DESCRIPTION:
     Constructs and configures an instance of Log from Settings.

     @throws UtilityException
    */
   private Log() throws UtilityException {
	   
      // get Log properties from Settings
      Settings settings = Settings.getInstance();
      
      String key;
      String value;
      String defaultValue;
      
      // log.level = level used to control logging output
      key = SettingsKeys.Log_Level;
      defaultValue = LogLevel.WARNING.toString();
      value = settings.getProperty(key,defaultValue);
      try {
         this.level = LogLevel.valueOf(value.trim().toUpperCase());
      } catch (IllegalArgumentException iae) {
         throw new UtilityException(key+": "+value+" is not a valid log level.",iae);
      }

      // log.consoleEnabled = flag to enable/disable logging via console handler
      key = SettingsKeys.Log_ConsoleEnabled;
      defaultValue = "false";
      value = settings.getProperty(key,defaultValue).trim().toLowerCase();
      if (value.equals("true"))
         this.consoleEnabled = true;
      else if (value.equals("false")) 
         this.consoleEnabled = false;
      else 
         throw new UtilityException(key+": "+value+" is not a valid boolean value.");
      
      // log.fileEnabled = flag to enable/disable logging via file handler
      key = SettingsKeys.Log_FileEnabled;
      defaultValue = "true";
      value = settings.getProperty(key,defaultValue).trim().toLowerCase();
      if (value.equals("true"))
         this.fileEnabled = true;
      else if (value.equals("false")) 
         this.fileEnabled = false;
      else 
         throw new UtilityException(key+": "+value+" is not a valid boolean value.");
       
      // log.fileLimit = maximum size of each log file
      key = SettingsKeys.Log_FileLimit;
      defaultValue = "10000000";
      value = settings.getProperty(key,defaultValue);
      try {
         this.fileLimit = Integer.parseInt(value);
      } catch (NumberFormatException nfe) {
         throw new UtilityException(key+": "+value+" is not a valid integer value.",nfe);
      }
      
      // log.fileCount = number of rotated log files to cycle through
      key = SettingsKeys.Log_FileCount;
      defaultValue = "3";
      value = settings.getProperty(key,defaultValue);
      try {
         this.fileCount = Integer.parseInt(value);
      } catch (NumberFormatException nfe) {
         throw new UtilityException(key+": "+value+" is not a valid integer value.",nfe);
      }
         
      // log.fileAppend = flag to enable/disable file handler append mode
      key = SettingsKeys.Log_FileAppend;
      defaultValue = "true";
      value = settings.getProperty(key,defaultValue).trim().toLowerCase();
      if (value.equals("true"))
         this.fileAppend = true;
      else if (value.equals("false")) 
         this.fileAppend = false;
      else 
         throw new UtilityException(key+": "+value+" is not a valid boolean value.");
      
      // log.filePattern = pattern for generating log file names
      key = SettingsKeys.Log_FilePattern;
      defaultValue = "%t/mse.%g.log";
      this.filePattern = settings.getProperty(key,defaultValue);
      
      // map our log levels to Logger levels
      this.levelMap = new EnumMap<LogLevel,Level>(LogLevel.class);
      this.levelMap.put(LogLevel.OFF,Level.OFF);
      this.levelMap.put(LogLevel.ERROR,Level.SEVERE);
      this.levelMap.put(LogLevel.WARNING,Level.WARNING);
      this.levelMap.put(LogLevel.INFO,Level.INFO);
      this.levelMap.put(LogLevel.DEBUG,Level.FINE);
      
      // remove default handlers (console, etc.) from root logger
      Logger root = Logger.getLogger("");
      Handler[] handlers = root.getHandlers();
      for (Handler handler : handlers) {
         root.removeHandler(handler);
      }
      
      // create java logger
      this.logger = Logger.getLogger("com.mse.utilities.Log");
      this.logger.setLevel(levelMap.get(this.level));
    
      // configure handlers
      if (this.level != LogLevel.OFF) {
         
         // create a file handler
         if (this.fileEnabled) { 
            try {
               FileHandler fileHandler;
               fileHandler = new FileHandler(filePattern,fileLimit,fileCount,fileAppend);
               fileHandler.setFormatter(new LogFormatter());
               fileHandler.setLevel(levelMap.get(this.level));
               this.logger.addHandler(fileHandler);
            } catch (IOException ioe) {
               throw new UtilityException("File handler I/O error.",ioe);
            } catch (IllegalArgumentException iae) {
               throw new UtilityException("File handler illegal argument.");
            }
         }

         // create a console handler
         if (this.consoleEnabled) {
            ConsoleHandler consoleHandler = new ConsoleHandler();
            consoleHandler.setFormatter(new LogFormatter());
            consoleHandler.setLevel(levelMap.get(this.level));
            this.logger.addHandler(consoleHandler);
         }
      }
   }

   /**
     DESCRIPTION:
     This method initializes the Log and creates its singleton instance. 
     
     @throws UtilityException if initialization fails.
    */
   public static void init() throws UtilityException {
      try {
         if (instance == null) instance = new Log();
      } catch (UtilityException ue) {
         throw new UtilityException("Log initialization failed - "+ue.getMessage(),ue);
      } catch(Throwable t) {
         throw new UtilityException("Log Initialization Failed",t);
      }
   }
   
   /**
     DESCRIPTION:
     This method returns the singleton instance.The init() method
     must be called before obtaining the instance.
   
     @return Log instance 
     @throws RuntimeException if instance is null (not initialized). 
    */
   public static synchronized Log getInstance() {
      
      if (instance == null) {
         throw new RuntimeException("Log instance is null");
      }

      return instance;
   }
   
   /**
     DESCRIPTION:
     This method disables the console log handler if it is enabled.
    */
   public synchronized void disableConsoleLogging() {
      Handler[] handlers = this.logger.getHandlers();
      for (Handler handler : handlers) {
         if (handler instanceof ConsoleHandler) {
            this.logger.removeHandler(handler);
         }
      }
   }
   
   /**
     DESCRIPTION:
     This method appends a debug message to the log.
 
     @param msg     message string to append to the log
    */
   public void appendDebug(String msg) {
      Object[] parameters = new Object[2];
      parameters[0] = "DEBUG -";
      parameters[1] = "";
      logger.log(levelMap.get(LogLevel.DEBUG),msg,parameters);
   }
   
   /**
     DESCRIPTION:
     This method appends a debug message and stack trace to the log.
   
     @param msg         message string to append to the log
     @param throwable   exception to append to log as stack trace
    */
   public void appendDebug(String msg, Throwable throwable) {
      Object[] parameters = new Object[2];
      parameters[0] = "DEBUG -";
      parameters[1] = "\n" + getStackTrace(throwable);
      logger.log(levelMap.get(LogLevel.DEBUG),msg,parameters);
   }
   
   /**
     DESCRIPTION:
     This method appends an information message to the log.
   
     @param msg     message string to append to the log
    */
   public void appendInfo(String msg) {
      Object[] parameters = new Object[2];
      parameters[0] = "INFO -";
      parameters[1] = "";
      logger.log(levelMap.get(LogLevel.INFO),msg,parameters);
   }
   
   /**
     DESCRIPTION:
     This method appends an information message and stack trace to the log.

     @param msg         message string to append to the log
     @param throwable   exception to append to log as stack trace
    */
   public void appendInfo(String msg, Throwable throwable) {
      Object[] parameters = new Object[2];
      parameters[0] = "INFO -";
      parameters[1] = "\n" + getStackTrace(throwable);
      logger.log(levelMap.get(LogLevel.INFO),msg,parameters);
   }
   
   /**
     DESCRIPTION:
     This method appends a warning message to the log.

     @param msg     message string to append to the log
    */
   public void appendWarning(String msg) {
      Object[] parameters = new Object[2];
      parameters[0] = "WARNING -";
      parameters[1] = "";
      logger.log(levelMap.get(LogLevel.WARNING),msg,parameters);
   }
   
   /**
     DESCRIPTION:
     This method appends a warning message and stack trace to the log.

     @param msg         message string to append to the log
     @param throwable   exception to append to log as stack trace
    */
   public void appendWarning(String msg, Throwable throwable) {
      Object[] parameters = new Object[2];
      parameters[0] = "WARNING -";
      parameters[1] = "\n" + getStackTrace(throwable);
      logger.log(levelMap.get(LogLevel.WARNING),msg,parameters);
   }
   
   /**
     DESCRIPTION:
     This method appends an error message to the log.
   
     @param msg     message string to append to the log
    */
   public void appendError(String msg) {
      Object[] parameters = new Object[2];
      parameters[0] = "ERROR -";
      parameters[1] = "";
      logger.log(levelMap.get(LogLevel.ERROR),msg,parameters);
   }
   
   /**
     DESCRIPTION:
     This method appends an error message and stack trace to the log.
   
     @param msg         message string to append to the log
     @param throwable   exception to append to log as stack trace
    */
   public void appendError(String msg, Throwable throwable) {
      Object[] parameters = new Object[2];
      parameters[0] = "ERROR -";
      parameters[1] = "\n" + getStackTrace(throwable);
      logger.log(levelMap.get(LogLevel.ERROR),msg,parameters);
   }
   
   /**
     DESCRIPTION:
     This method returns a stack trace for an exception as a string.
   
     @param throwable   exception to obtain stack trace from
     @return            stack trace string
    */
   private String getStackTrace(Throwable throwable) {
      final Writer result = new StringWriter();
      final PrintWriter printWriter = new PrintWriter(result);
      throwable.printStackTrace(printWriter);
      return result.toString().trim();
   }
  
}

